# -*- coding: utf-8 -*-
#!/usr/bin/python2.4
#
# Copyright 2011 Google Inc. All Rights Reserved.

"""WebRTC Demo

This module demonstrates the WebRTC API by implementing a simple video chat app.
"""

import cgi
import json
import logging
import os
import random
import threading
import time

import jinja2
import webapp2
from google.appengine.api import app_identity
from google.appengine.api import memcache
from google.appengine.api import urlfetch

import analytics
import analytics_page
import compute_page
import constants

# (关键修复) 强制 Python 的 logging 模块使用容器的本地时区（已在 Dockerfile 中设为 UTC+8），
# 而不是 GAE 开发服务器默认的 UTC 时间。
# 这样，Python 应用的日志时间戳就能与其他服务（Go, stunnel）保持一致。
logging.Formatter.converter = time.localtime

jinja_environment = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)))


def generate_random(length):
  """生成一个指定长度的随机数字字符串。"""
  word = ''
  for _ in range(length):
    word += random.choice('0123456789')
  return word

# HD is on by default for desktop Chrome, but not Android or Firefox (yet)
def get_hd_default(user_agent):
  if 'Android' in user_agent or not 'Chrome' in user_agent:
    return 'false'
  return 'true'

# iceServers will be filled in by the TURN HTTP request.
def make_pc_config(ice_transports, ice_server_override):
  config = {
  'iceServers': [],
  'bundlePolicy': 'max-bundle',
  'rtcpMuxPolicy': 'require'
  };
  if ice_server_override:
    config['iceServers'] = ice_server_override
  if ice_transports:
    config['iceTransports'] = ice_transports
  return config

def add_media_track_constraint(track_constraints, constraint_string):
  """解析单个媒体轨道约束字符串，并添加到约束集。"""
  tokens = constraint_string.split(':')
  mandatory = True
  if len(tokens) == 2:
    # If specified, e.g. mandatory:minHeight=720, set mandatory appropriately.
    mandatory = (tokens[0] == 'mandatory')
  else:
    # Otherwise, default to mandatory, except for goog constraints, which
    # won't work in other browsers.
    mandatory = not tokens[0].startswith('goog')

  tokens = tokens[-1].split('=')
  if len(tokens) == 2:
    if mandatory:
      track_constraints['mandatory'][tokens[0]] = tokens[1]
    else:
      track_constraints['optional'].append({tokens[0]: tokens[1]})
  else:
    logging.error('Ignoring malformed constraint: ' + constraint_string)

def make_media_track_constraints(constraints_string):
  if not constraints_string or constraints_string.lower() == 'true':
    track_constraints = True
  elif constraints_string.lower() == 'false':
    track_constraints = False
  else:
    track_constraints = {'mandatory': {}, 'optional': []}
    for constraint_string in constraints_string.split(','):
      add_media_track_constraint(track_constraints, constraint_string)

  return track_constraints

def make_media_stream_constraints(audio, video, firefox_fake_device):
  stream_constraints = (
      {'audio': make_media_track_constraints(audio),
       'video': make_media_track_constraints(video)})
  if firefox_fake_device:
    stream_constraints['fake'] = True
  logging.info('正在应用媒体约束: ' + str(stream_constraints))
  return stream_constraints

def maybe_add_constraint(constraints, param, constraint):
  """根据参数值（'true'或'false'）向约束列表中添加一个布尔约束。"""
  if (param.lower() == 'true'):
    constraints['optional'].append({constraint: True})
  elif (param.lower() == 'false'):
    constraints['optional'].append({constraint: False})

  return constraints

def make_pc_constraints(dtls, dscp, ipv6):
  """构建对等连接（PeerConnection）的约束条件。"""
  constraints = {
      'optional': [
          # DtlsSrtpKeyAgreement 用于协商 SRTP 的密钥。
          {'DtlsSrtpKeyAgreement': str(dtls == 'true').lower()},
          # googDscp 是一个 Google 特定的选项，用于启用 DSCP（差分服务代码点），
          # 这可以帮助在网络中区分和优先处理实时音视频数据包。
          {'googDscp': str(dscp == 'true').lower()}
      ]
  }
  # 为了解决已知的兼容性问题，这里强制禁用了 IPv6。
  # 在 WebRTC 的早期阶段，IPv6 的支持并不完善，可能会导致连接失败。
  constraints['optional'].append({'googIPv6': False})
  return constraints

def maybe_use_https_host_url(request):
    """检查请求是否来自一个 HTTPS 代理，如果是，则修正 URL 协议。"""
    if request.get('wstls') == 'true' and request.scheme == 'http':
    # 这种组合通常意味着 AppRTC 运行在一个 stunnel 代理后面。
    # stunnel 接收外部的 HTTPS 请求，然后将其解密为 HTTP 请求转发给内部的 AppRTC 服务。
    # 因此，我们需要将 URL 从 http:// 修正为 https://，以确保客户端能正确连接。
      return request.host_url.replace('http:', 'https:')
    return request.host_url

def append_url_arguments(request, link):
  """将原始请求中的所有 URL 参数附加到一个新的链接上。"""
  arguments = request.arguments()
  if len(arguments) == 0:
    return link
  link += ('?' + cgi.escape(arguments[0], True) + '=' +
           cgi.escape(request.get(arguments[0]), True))
  for argument in arguments[1:]:
    link += ('&' + cgi.escape(argument, True) + '=' +
             cgi.escape(request.get(argument), True))
  return link

def get_wss_parameters(request):
  """为本地开发环境返回 WebSocket (WSS) 的 URL 和 POST URL。"""
  # 这里硬编码了本地 collider 服务的地址，并且未使用 TLS 加密。
  # 客户端将通过 ws://appr.tc:8089/ws 连接到 WebSocket 服务器。
  # Python 后端则通过 http://localhost:8089 向 collider 发送内部通信请求。
  wss_url = 'ws://appr.tc:8089/ws'
  wss_post_url = 'http://localhost:8089'
  return wss_url, wss_post_url

def get_version_info():
  """从 version_info.json 文件中读取并返回版本信息。"""
  try:
    path = os.path.join(os.path.dirname(__file__), 'version_info.json')
    f = open(path)
    if f is not None:
      try:
        return json.load(f)
      except ValueError as e:
        logging.warning('version_info.json 文件无法被解析: ' + str(e))
  except IOError as e:
    logging.info('version_info.json 文件无法被打开: ' + str(e))
  return None

# 根据请求中的查询参数，返回合适的房间参数。
# TODO(tkchin): 应该将查询参数的解析工作移到 JavaScript 代码中完成。
def get_room_parameters(request, room_id, client_id, is_initiator):
  error_messages = []
  warning_messages = []
  # 获取不带任何参数的基础 URL。
  base_url = request.path_url
  user_agent = request.headers['User-Agent']

  # 响应类型：HTML 或 JSON。
  response_type = request.get('t')
  # 允许哪些类型的 ICE 候选者。这对于强制通话通过 TURN 服务器中继非常有用，
  # 例如，可以设置为 it=relay。
  ice_transports = request.get('it')
  # 允许哪种 ICE 服务器传输协议（transport=）。例如，只使用 transport=<tt> 的 TURN URL。
  # 这对于强制会话使用 TURN/TCP 非常有用，可以设置为 it=relay&tt=tcp。
  ice_server_transports = request.get('tt')
  # 一个 HTTP 服务器地址，用于查找合适的 ICE 服务器。
  # 这是基于 IETF 的一个草案（draft-uberti-rtcweb-turn-rest-00）实现的。
  ice_server_base_url = request.get('ts', default_value =
      constants.ICE_SERVER_BASE_URL)

  # 使用 "audio" 和 "video" 参数来设置媒体流的约束条件。
  # 详细定义请见: http://goo.gl/V7cZg
  #
  # "true" 和 "false" 会被识别并解析为布尔值，例如:
  #   "?audio=true&video=false" (发起一个纯音频通话)
  #   "?audio=false" (发起一个纯视频通话)
  # 如果不指定，流约束默认为 True。
  #
  # 要指定更详细的媒体轨道约束，可以传入一个以逗号分隔的键值对列表，键和值之间用 "=" 分隔。例如:
  #   "?audio=googEchoCancellation=false,googAutoGainControl=true"
  #   (禁用回声消除，并启用自动增益控制)
  #
  #   "?video=minWidth=1280,minHeight=720,googNoiseReduction=true"
  #   (将最小分辨率设为 1280x720，并启用降噪)
  #
  # 以 "goog" 开头的键将被添加到 "optional"（可选）约束中；所有其他的键将被添加到 "mandatory"（强制）约束中。
  # 要覆盖此默认行为，可以在每个键前添加 "mandatory" 或 "optional" 前缀，例如:
  #   "?video=optional:minWidth=1280,optional:minHeight=720,mandatory:googNoiseReduction=true"
  #   (尝试使用 1280x720 分辨率，但如果无法满足也接受较低分辨率；但必须成功启用降噪，否则失败)
  #
  # 音频相关的键定义在: talk/app/webrtc/localaudiosource.cc
  # 视频相关的键定义在: talk/app/webrtc/videosource.cc
  audio = request.get('audio')
  video = request.get('video')

  # 传入 firefox_fake_device=1 会在媒体约束中加入 fake: true，
  # 这将使 Firefox 使用其内置的虚拟设备进行测试。
  firefox_fake_device = request.get('firefox_fake_device')

  # "hd" 参数是一个简写，用于决定是否以 720p 分辨率打开摄像头。
  # 如果没有提供该值，则使用平台特定的默认值。
  # 当默认启用高清时，我们会使用可选（optional）约束，以防摄像头实际不支持高清模式。
  hd = request.get('hd').lower()
  if hd and video:
    message = '警告: "hd" 参数覆盖了已有的 "video" 参数: ' + video
    logging.warning(message)
    # HTML 模板是 UTF-8 编码，所以要确保这里的字符串也是 UTF-8。
    warning_messages.append(message.encode('utf-8'))
  if hd == 'true':
    video = 'mandatory:minWidth=1280,mandatory:minHeight=720'
  elif not hd and not video and get_hd_default(user_agent) == 'true':
    video = 'optional:minWidth=1280,optional:minHeight=720'

  if request.get('minre') or request.get('maxre'):
    message = ('"minre" 和 "maxre" 参数已不再支持。请改用 "video" 参数。')
    logging.warning(message)
    # HTML 模板是 UTF-8 编码，所以要确保这里的字符串也是 UTF-8。
    warning_messages.append(message.encode('utf-8'))

  # 用于控制各种网络特性的选项。
  dtls = request.get('dtls')
  dscp = request.get('dscp')
  ipv6 = 'false'

  debug = request.get('debug')
  if debug == 'loopback':
    # 对于环回（loopback）测试，需要禁用 DTLS，因为它在这种场景下无法工作。
    dtls = 'false'
    include_loopback_js = '<script src="/js/loopback.js"></script>'
  else:
    include_loopback_js = ''

  # TODO(tkchin): 我们希望在初次 GET 请求时就提供一个 ICE 请求 URL，
  # 但在用户加入房间之前，我们还没有 client_id。
  # 目前的临时方案是生成一个随机 ID，但未来应该改进这个逻辑。
  username = client_id if client_id is not None else generate_random(9)
  if len(ice_server_base_url) > 0:
    api_key = request.get('apikey', default_value=constants.ICE_SERVER_API_KEY)
    ice_server_url = constants.ICE_SERVER_URL_TEMPLATE % \
        (ice_server_base_url, api_key)
  else:
    ice_server_url = ''

  # 如果定义了 ice_server_override，它将覆盖从 ICE 服务器提供商获取的设置，
  # 并直接使用指定的 TURN 服务器。
  ice_server_override = constants.ICE_SERVER_OVERRIDE

  pc_config = make_pc_config(ice_transports, ice_server_override)
  pc_constraints = make_pc_constraints(dtls, dscp, ipv6)
  offer_options = {}
  media_constraints = make_media_stream_constraints(audio, video,
                                                    firefox_fake_device)
  wss_url, wss_post_url = get_wss_parameters(request)

  # 检查环境变量，判断是否需要绕过“加入房间”的确认步骤。
  bypass_join_confirmation = 'BYPASS_JOIN_CONFIRMATION' in os.environ and \
      os.environ['BYPASS_JOIN_CONFIRMATION'] == 'True'

  params = {
    'header_message': constants.HEADER_MESSAGE,
    'error_messages': error_messages,
    'warning_messages': warning_messages,
    'is_loopback' : json.dumps(debug == 'loopback'),
    'pc_config': json.dumps(pc_config),
    'pc_constraints': json.dumps(pc_constraints),
    'offer_options': json.dumps(offer_options),
    'media_constraints': json.dumps(media_constraints),
    'ice_server_url': ice_server_url,
    'ice_server_transports': ice_server_transports,
    'include_loopback_js' : include_loopback_js,
    'wss_url': wss_url,
    'wss_post_url': wss_post_url,
    'bypass_join_confirmation': json.dumps(bypass_join_confirmation),
    'version_info': json.dumps(get_version_info())
  }

  if room_id is not None:
    room_link = maybe_use_https_host_url(request) + '/r/' + room_id
    room_link = append_url_arguments(request, room_link)
    params['room_id'] = room_id
    params['room_link'] = room_link
  if client_id is not None:
    params['client_id'] = client_id
  if is_initiator is not None:
    params['is_initiator'] = json.dumps(is_initiator)
  return params

# 目前，我们认为 (room_id, client_id) 对是“唯一”的，但 client_id 本身不是。
# 然而，系统并未强制实施唯一性，如果随机数生成器产生了非唯一的数字，可能会发生意想不到的问题。
# 我们还有一个特殊的环回（loopback）客户端 ID。
# TODO(tkchin): 需要以一种唯一的方式生成房间/客户端 ID，同时能正确处理环回场景。
class Client:
  """代表一个加入房间的客户端。"""
  def __init__(self, is_initiator):
    self.is_initiator = is_initiator
    self.messages = []
  def add_message(self, msg):
    """向该客户端的消息队列中添加一条消息。"""
    self.messages.append(msg)
  def clear_messages(self):
    """清空该客户端的所有已缓存消息。"""
    self.messages = []
  def set_initiator(self, initiator):
    """设置该客户端是否为通话发起方。"""
    self.is_initiator = initiator
  def __str__(self):
    return '{%r, %d}' % (self.is_initiator, len(self.messages))

class Room:
  """代表一个通话房间。"""
  def __init__(self):
    self.clients = {}
  def add_client(self, client_id, client):
    """向房间中添加一个新客户端。"""
    self.clients[client_id] = client
  def remove_client(self, client_id):
    """从房间中移除一个客户端。"""
    del self.clients[client_id]
  def get_occupancy(self):
    """获取房间当前的客户端数量。"""
    return len(self.clients)
  def has_client(self, client_id):
    """检查房间中是否存在指定 ID 的客户端。"""
    return client_id in self.clients
  def get_client(self, client_id):
    """获取指定 ID 的客户端对象。"""
    return self.clients[client_id]
  def get_other_client(self, client_id):
    """获取房间中除指定客户端外的另一个客户端。"""
    for key, client in self.clients.items():
      if key is not client_id:
        return client
    return None
  def __str__(self):
    return str(self.clients.keys())

def get_memcache_key_for_room(host, room_id):
  """为指定的房间生成一个在 memcache 中唯一的键。"""
  return '%s/%s' % (host, room_id)

def add_client_to_room(request, room_id, client_id, is_loopback):
  """将一个客户端添加到房间中。这是一个核心函数，处理房间的创建、加入和状态同步。"""
  key = get_memcache_key_for_room(request.host_url, room_id)
  memcache_client = memcache.Client()
  error = None
  retries = 0
  room = None
  # 使用一个“比较并设置”（Compare-And-Set, CAS）的重试循环来保证操作的原子性，
  # 这对于处理并发请求至关重要，可以防止多个用户同时修改同一个房间状态时产生的数据冲突。
  while True:
    is_initiator = None
    messages = []
    room_state = ''
    room = memcache_client.gets(key)
    if room is None:
      # 如果房间不存在，我们需要先创建一个新的 Room 对象并存入 memcache。
      # 这里需要先 'set' 再 'gets' 一次，这是为了让 CAS 机制能够正常工作。
      if not memcache_client.set(key, Room()):
        logging.warning('memcache.Client.set 操作失败，键为: ' + key)
        error = constants.RESPONSE_ERROR
        break
      room = memcache_client.gets(key)

    occupancy = room.get_occupancy()
    if occupancy >= 2:
      error = constants.RESPONSE_ROOM_FULL
      break
    if room.has_client(client_id):
      error = constants.RESPONSE_DUPLICATE_CLIENT
      break

    if occupancy == 0:
      # 如果房间是空的，那么这个客户端就是第一个加入者，即“发起方”（initiator）。
      is_initiator = True
      room.add_client(client_id, Client(is_initiator))
      if is_loopback:
        # 如果是环回测试，我们还需要额外添加一个虚拟的 loopback 客户端。
        room.add_client(constants.LOOPBACK_CLIENT_ID, Client(False))
    else:
      # 如果房间里已经有人，那么这个客户端就是“加入方”。
      is_initiator = False
      other_client = room.get_other_client(client_id)
      # 获取并准备好转发给新加入者的、已缓存的消息。
      messages = other_client.messages
      room.add_client(client_id, Client(is_initiator))
      # 消息即将被转发，清空对方客户端的消息缓存。
      other_client.clear_messages()

    if memcache_client.cas(key, room, constants.ROOM_MEMCACHE_EXPIRATION_SEC):
      logging.info('已将客户端 %s 添加到房间 %s，重试次数 = %d' \
          %(client_id, room_id, retries))

      if room.get_occupancy() == 2:
        # 当房间满员时，上报一个分析事件。
        analytics.report_event(analytics.EventType.ROOM_SIZE_2,
                               room_id,
                               host=request.host)
      success = True
      break
    else:
      # 如果 CAS 操作失败（意味着在gets和cas之间，有其他请求修改了房间数据），则增加重试次数并再次循环。
      retries = retries + 1
  return {'error': error, 'is_initiator': is_initiator,
          'messages': messages, 'room_state': str(room)}

def remove_client_from_room(host, room_id, client_id):
  """从房间中移除一个客户端。"""
  key = get_memcache_key_for_room(host, room_id)
  memcache_client = memcache.Client()
  retries = 0
  # 同样使用 CAS 重试循环来保证原子性。
  while True:
    room = memcache_client.gets(key)
    if room is None:
      logging.warning('remove_client_from_room: 未知的房间 ' + room_id)
      return {'error': constants.RESPONSE_UNKNOWN_ROOM, 'room_state': None}
    if not room.has_client(client_id):
      logging.warning('remove_client_from_room: 在房间 %s 中找不到客户端 %s' \
          %(room_id, client_id))
      return {'error': constants.RESPONSE_UNKNOWN_CLIENT, 'room_state': None}

    room.remove_client(client_id)
    if room.has_client(constants.LOOPBACK_CLIENT_ID):
      room.remove_client(constants.LOOPBACK_CLIENT_ID)
    if room.get_occupancy() > 0:
      # 如果房间里还有人，将剩下的那个人设为新的“发起方”。
      room.get_other_client(client_id).set_initiator(True)
    else:
      # 如果房间空了，就将 room 设为 None，以便在 CAS 操作后从 memcache 中删除它。
      room = None

    if memcache_client.cas(key, room, constants.ROOM_MEMCACHE_EXPIRATION_SEC):
      logging.info('已从房间 %s 中移除客户端 %s，重试次数=%d' \
          %(client_id, room_id, retries))
      return {'error': None, 'room_state': str(room)}
    retries = retries + 1

def save_message_from_client(host, room_id, client_id, message):
  """保存从客户端发来的消息。"""
  text = None
  try:
      text = message.encode(encoding='utf-8', errors='strict')
  except Exception as e:
    return {'error': constants.RESPONSE_ERROR, 'saved': False}

  key = get_memcache_key_for_room(host, room_id)
  memcache_client = memcache.Client()
  retries = 0
  # 同样使用 CAS 重试循环。
  while True:
    room = memcache_client.gets(key)
    if room is None:
      logging.warning('未知的房间: ' + room_id)
      return {'error': constants.RESPONSE_UNKNOWN_ROOM, 'saved': False}
    if not room.has_client(client_id):
      logging.warning('未知的客户端: ' + client_id)
      return {'error': constants.RESPONSE_UNKNOWN_CLIENT, 'saved': False}
    if room.get_occupancy() > 1:
      # 如果房间里已经有另一个人了，说明对方已经加入，我们不需要再为这个客户端缓存消息。
      # 这条消息应该被立即转发，所以这里返回 'saved': False。
      return {'error': None, 'saved': False}

    client = room.get_client(client_id)
    client.add_message(text)
    if memcache_client.cas(key, room, constants.ROOM_MEMCACHE_EXPIRATION_SEC):
      logging.info('已为客户端 %s:%s 在房间 %s 中保存消息，重试次数=%d' \
          %(client_id, str(client), room_id, retries))
      return {'error': None, 'saved': True}
    retries = retries + 1

class LeavePage(webapp2.RequestHandler):
  """处理客户端离开房间的请求。"""
  def post(self, room_id, client_id):
    result = remove_client_from_room(
        self.request.host_url, room_id, client_id)
    if result['error'] is None:
      logging.info('房间 ' + room_id + ' 的当前状态: ' + result['room_state'])

class MessagePage(webapp2.RequestHandler):
  """处理客户端发送消息的请求。"""
  def write_response(self, result):
    content = json.dumps({ 'result' : result })
    self.response.write(content)

  def send_message_to_collider(self, room_id, client_id, message):
    # 硬编码指向本地的 collider 信令服务。
    wss_url = 'http://localhost:8089/' + room_id + '/' + client_id
    result = urlfetch.fetch(url=wss_url,
                            payload=message,
                            method=urlfetch.POST)
    if result.status_code != 200:
      logging.error(
          '向 collider 发送消息失败: %d' % (result.status_code))
      # TODO(tkchin): 需要实现更好的错误处理机制。
      self.error(500)
      return
    self.write_response(constants.RESPONSE_SUCCESS)

  def post(self, room_id, client_id):
    message_json = self.request.body
    result = save_message_from_client(
        self.request.host_url, room_id, client_id, message_json)
    if result['error'] is not None:
      self.write_response(result['error'])
      return
    if not result['saved']:
      # 如果消息没有被保存（因为房间里已经有另一个人了），则需要将消息转发给对方。
      # 这个转发操作在锁之外进行。
      # 注意：在本地开发服务器上，由于缺少正确的 SSL 验证证书，这个操作可能会失败。
      # 注意：环回（loopback）场景也会走这个代码路径。
      # TODO(tkchin): 考虑在这里使用异步 fetch。
      self.send_message_to_collider(room_id, client_id, message_json)
    else:
      self.write_response(constants.RESPONSE_SUCCESS)

class JoinPage(webapp2.RequestHandler):
  """处理客户端加入房间的请求。"""
  def write_response(self, result, params, messages):
    # TODO(tkchin): 需要清理一下响应格式。为了简单起见，暂时将所有东西都放在 params 里。
    params['messages'] = messages
    self.response.write(json.dumps({
      'result': result,
      'params': params
    }))

  def write_room_parameters(self, room_id, client_id, messages, is_initiator):
    params = get_room_parameters(self.request, room_id, client_id, is_initiator)
    self.write_response('SUCCESS', params, messages)

  def post(self, room_id):
    client_id = generate_random(8)
    is_loopback = self.request.get('debug') == 'loopback'
    result = add_client_to_room(self.request, room_id, client_id, is_loopback)
    if result['error'] is not None:
      logging.info('将客户端添加到房间时出错: ' + result['error'] + \
          ', 房间状态=' + result['room_state'])
      self.write_response(result['error'], {}, [])
      return

    self.write_room_parameters(
        room_id, client_id, result['messages'], result['is_initiator'])
    logging.info('用户 ' + client_id + ' 已加入房间 ' + room_id)
    logging.info('房间 ' + room_id + ' 的状态为 ' + result['room_state'])

class MainPage(webapp2.RequestHandler):
  """处理主页（根路径）的请求。"""
  def write_response(self, target_page, params={}):
    template = jinja_environment.get_template(target_page)
    content = template.render(params)
    self.response.out.write(content)

  def get(self):
    """渲染 index_template.html 页面。"""
    checkIfRedirect(self);
    # 从请求中解析出参数。
    params = get_room_parameters(self.request, None, None, None)
    # 返回的参数中不会包含 room_id/room_link，
    # 这样客户端就会显示用于选择房间的初始页面。
    self.write_response('index_template.html', params)

class RoomPage(webapp2.RequestHandler):
  """处理特定房间页面的请求。"""
  def write_response(self, target_page, params={}):
    template = jinja_environment.get_template(target_page)
    content = template.render(params)
    self.response.out.write(content)

  def get(self, room_id):
    """渲染 index_template.html 或 full_template.html 页面。"""
    checkIfRedirect(self)
    # 检查房间是否已满。
    room = memcache.get(
        get_memcache_key_for_room(maybe_use_https_host_url(self.request), room_id))
    if room is not None:
      logging.info('房间 ' + room_id + ' 的状态为 ' + str(room))
      if room.get_occupancy() >= 2:
        logging.info('房间 ' + room_id + ' 已满')
        self.write_response('full_template.html')
        return
    # 从请求中解析出房间参数。
    params = get_room_parameters(self.request, room_id, None, None)
    # 返回的参数中会包含 room_id/room_link，
    # 这样客户端就会直接进入指定的房间。
    self.write_response('index_template.html', params)

class ParamsPage(webapp2.RequestHandler):
  """处理获取房间无关参数的请求。"""
  def get(self):
    # 返回与具体房间无关的通用参数。
    params = get_room_parameters(self.request, None, None, None)
    self.response.write(json.dumps(params))

def checkIfRedirect(self):
  """检查当前访问的域名是否需要重定向到主域名。"""
  parsed_args = ''
  if self.request.headers['Host'] in constants.REDIRECT_DOMAINS:
    for argument in self.request.arguments():
      parameter = '=' + self.request.get(argument)
      if parsed_args == '':
        parsed_args += '?'
      else:
        parsed_args += '&'
      parsed_args += argument + parameter
    redirect_url = constants.REDIRECT_URL + self.request.path + parsed_args
    webapp2.redirect(redirect_url, permanent=True, abort=True)

class IceConfigurationPage(webapp2.RequestHandler):
  """处理获取 ICE 服务器配置的请求。"""
  def post(self):
    ice_config = {}
    if constants.ICE_SERVER_OVERRIDE:
      ice_config = {"iceServers": constants.ICE_SERVER_OVERRIDE}
    else:
      ice_config = {"iceServers": [{"urls": constants.ICE_SERVER_URLS}]}
    self.response.write(json.dumps(ice_config))


app = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/a/', analytics_page.AnalyticsPage),
    ('/compute/(\w+)/(\S+)/(\S+)', compute_page.ComputePage),
    ('/join/([a-zA-Z0-9-_]+)', JoinPage),
    ('/leave/([a-zA-Z0-9-_]+)/([a-zA-Z0-9-_]+)', LeavePage),
    ('/message/([a-zA-Z0-9-_]+)/([a-zA-Z0-9-_]+)', MessagePage),
    ('/params', ParamsPage),
    ('/v1alpha/iceconfig', IceConfigurationPage),
    ('/r/([a-zA-Z0-9-_]+)', RoomPage),
], debug=True)
